package nessie;

import java.io.BufferedInputStream;
import java.io.IOException;

/**
 * this is the emulator's memory handler.
 * memory handling is divided into 3 parts:
 * -addressing modes:
 *   the 6502 has various memory modes, which produces multiple
 *   versions of the certain instructions(e.g ADD). however, the operation that
 *   is executed by said instruction, almost always stays the same. the only difference
 *   is how and where the data is fetched from.
 *   so instead of creating duplicate instructions, i chose to separate the
 *   memory fetching from the execution.
 * -mirroring:
 *   the NES RAM uses partial address decoding, which means that certain locations
 *   are mirrored at other locations. meaning that a write to location X for example,
 *   does a duplicate to location X+8, x+16, x+24....x+(n*8).
 *   because the address is only partially decoded, it is written to all variations
 *   of the address. so a write to address 0x15(10101) with a 4 bit partial
 *   decoder writes to binary addresses 10101 and 00101.
 *   in this case i've chosen to redirect the mirror calls rather than mirror
 *   the writes(cheaper).
 * -port handling:
 *   the correct term for this is memory mapped i/o, but port handling sounds
 *   more correct(and cooler) to what is actually being done here.
 *   for instance, memory locations become the go-between between the CPU and the PPU.
 *   meaning that instead of writing and loading 2 different programs(1 for CPU
 *   and 1 for PPU), all the communication goes through the CPU(according to a
 *   set of predefined rules). for example(this isn't how the actual PPU works),
 *   we make a rule in the PPU that address 0x100 holds the background color,
 *   and while the PPU is enabled, continuously draw the background color at
 *   that address.
 */
public class Memory {

    /**
     * G E N E R A L
     */
    public static final int _ROM = 0x8000;
    private static final int _SMALLROM = 0xC000;
    private static final int _8KB = 1024 * 8;
    private static final int _16KB = _8KB * 2;
    private static final int TRAINER = 512;
    private static final byte[] HEADER = new byte[16];
    private static final byte[] RAM = new byte[0x10000];

    private Memory() {
//        RAM = new byte[0x10000];//64kb
    }

    /**@return next 8bits*/
    private static byte getNextByte() {
        ++Registers.PC;
        return RAM[Registers.getUnsignerPC()];
    }

    /**@return next 16bits in low endian*/
    private static short getNextWord() {
        ++Registers.PC;
        int a = RAM[Registers.getUnsignerPC()] & 0xFF;
        ++Registers.PC;
        int b = (RAM[Registers.getUnsignerPC()] & 0xFF) << 8;//one byte(8bits) to the left

        short word = (short) (a + b);
        return word;
    }

    public static void loadRom(BufferedInputStream inputStream) throws IOException {
        {
            //inputStream = new BufferedInputStream(new FileInputStream(file));
            //exec header
            if (inputStream.available() > 16) {
                //TODO:add ines header check
                inputStream.read(Memory.HEADER);
            } else {
                throw new UnsupportedOperationException("UNKNOWN FILETYPE!");
            }

            if (Memory.HEADER[6] > 1) {//NROM check
                System.err.println(" <<<<UNSUPPORTED MAPPER!!>>>> ");
                throw new UnsupportedOperationException(" ONLY NROM SUPPORTED ");
            }

            //horizontal mirroring
            if (Memory.HEADER[6] == 0) {
                PPU.VERTICAL_MIRROR = false;
            } else //vertical mirroring
            if (Memory.HEADER[6] == 1) {
                PPU.VERTICAL_MIRROR = true;
            }
            //skip trainer
            if ((Memory.HEADER[6] & 4) == 4) {
                while (inputStream.skip(TRAINER) < TRAINER);
            }
            //PRG-ROM
            final byte[] ROM = new byte[Memory._16KB * Memory.HEADER[4]];
            while (inputStream.read(ROM) < ROM.length);

            //copy ROM to RAM @C000
            if (Memory.HEADER[4] == 1) {
                //smaller roms are loaded into both memory banks
                System.arraycopy(ROM, 0, Memory.RAM, Memory._ROM, ROM.length);
                System.arraycopy(ROM, 0, Memory.RAM, Memory._SMALLROM, ROM.length);
            } else {
                //copy ROM to RAM @8000
                System.arraycopy(ROM, 0, Memory.RAM, Memory._ROM, ROM.length);
            }

            //VROM
            byte[] VROM = new byte[Memory._8KB * Memory.HEADER[5]];
            if (VROM.length > 0) {//if CHR-ROM exists
                //TODO:add CHR-ROM/CHR-RAM check
                while (inputStream.read(VROM) < VROM.length);
                PPU.loadVRAM(VROM);
            }
        }
    }
    /**
     * 
     * P O R T   H A N D L I N G
     *
     */
    /**0x4016*/
    private static final int P1_CONTROLLER = 0x4016;
    /**0x4017*/
    private static final int P2_CONTROLLER = 0x4017;

    public static synchronized byte readByte(int address) {
        final int unsignedAddress = resolveMirror(address &= 0xFFFF) & 0xFFFF;
        switch (unsignedAddress) {
            case 0x2002:
                byte data = PPU.getFlags();//0b1110_0000
                PPU.clearFIRST_WRITE_TOGGLE();
                PPU.clearVBL();
                return data;
            case 0x2004:
                //do not increment pointer
                return PPU.get2004();
            case 0x2007:
                return PPU.get2007();
            case P1_CONTROLLER:
                return Controller.getP1Input();
            case P2_CONTROLLER:
                return 0x40;
        }
        return RAM[unsignedAddress];
    }

    ///TODO:MIRROR MIRROR ON THE WALL, WIE IST DAS UGLIEST MIRROR OF THEM ALL!
    public static synchronized byte writeByte(int address, byte data) {
        final int unsignedAddress = resolveMirror(address &= 0xFFFF) & 0xFFFF;
        final byte oldData = RAM[unsignedAddress];
        RAM[unsignedAddress] = data;
        switch (unsignedAddress) {
            case 0x2000:
                PPU.setPPUCTRL();
                break;
            case 0x2001:
                PPU.setPPUMASK();
                break;
            case 0x2003:
                PPU.set2003();
                break;
            case 0x2004:
                //do DO increment pointer
                PPU.set2004(data);
                break;
            case 0x2005:
                PPU.set2005();
                break;
            case 0x2006:
                PPU.set2006();//VRAM address x 2
                break;
            case 0x2007:
                PPU.set2007(data);//VRAM data
                break;
            case 0x4014:
                PPU.set4014();//DMA
                break;
            case P1_CONTROLLER:
                if (((data & 3) < 2) && ((oldData & 3) > 0)) {//switch from 1 to 0
                    Controller.setKeyArray();
                }
                break;
        }
        return data;
    }

    /**
     * M I R R O R I N G
     */
    /**
     * this function resolves the original address if the input address is mirrored
     * @param address
     * @return the unmirrored address
     */
    private static int resolveMirror(int address) {
        if (address >= 0x4000) {
            return address;
        }
        if (address < 0x2000) {//default
            return address % 0x800;
        }
        if (address < 0x4000) {
            return 0x2000 + (address % 8);
        }
        //never reached
        throw new UnsupportedOperationException("address out of bounds");
    }

    //TODO:ROM instead of RAM
    /**
     * A D D R E S S I N G
     * @return byte @ address
     */
    /**get next byte*/
    public static byte MODE_IMMEDIATE() {
        return getNextByte();
    }

    /**get next byte as memory location*/
    public static short MODE_ZERO_PAGE() {
        return getROMByte(ADDRESS_ZERO_PAGE());
    }

    /**get next byte as memory location and add X register*/
    public static short MODE_ZERO_PAGE_X() {
        return getROMByte(ADDRESS_ZERO_PAGE_X());
    }

    public static short MODE_ZERO_PAGE_Y() {//NES doesnt use this..but what the hell!
        return getROMByte(ADDRESS_ZERO_PAGE_Y());
    }

    /**
     * @return the BYTE at the absolute address(the address from the next 2
     * bytes)
     */
    public static short MODE_ABSOLUTE() {
        return getROMByte(ADDRESS_ABSOLUTE());
    }

    /**
     * @return the BYTE at the absolute X address(the address from the next 2
     * bytes plus the X register)
     */
    public static short MODE_ABSOLUTE_X() {
        int pc = Registers.getUnsignerPC();
        short ROMByte = getROMByte(ADDRESS_ABSOLUTE_X());
        isPageCrossed(pc);
        return ROMByte;
    }

    public static short MODE_ABSOLUTE_X_DUMMY_READ() {
        short address = getNextWord();

        int absoluteAddress = ((address + getX()) & 0xFFFF);
        int dummyAbsoluteAddress = ((address & 0xFF00) | (absoluteAddress & 0xFF));

        if (dummyAbsoluteAddress != absoluteAddress) {
            CPU.skipInstruction = true;//skip the instruction
            Registers.PC++;//move to next position
        }
        return readByte(dummyAbsoluteAddress);//but still do the read(i.e. clear vbl when reading 0x2002)
    }

    /**
     * @return the BYTE at the absolute Y address(the address from the next 2
     * bytes plus the Y register)
     */
    public static short MODE_ABSOLUTE_Y() {
        int pc = Registers.getUnsignerPC();
        short ROMByte = getROMByte(ADDRESS_ABSOLUTE_Y());
        isPageCrossed(pc);
        return ROMByte;
    }

    public static short MODE_ABSOLUTE_Y_DUMMY_READ() {
        short address = getNextWord();

        int absoluteAddress = ((address + getY()) & 0xFFFF);
        int dummyAbsoluteAddress = ((address & 0xFF00) | (absoluteAddress & 0xFF));

        if (dummyAbsoluteAddress != absoluteAddress) {
            CPU.skipInstruction = true;//skip the instruction
            Registers.PC++;//move to next position
        }
        return readByte(dummyAbsoluteAddress);//but still do the read(i.e. clear vbl when reading 0x2002)
    }

    /**
     * @return the address located at an address formed by the next 2 bytes. the
     * return address wraps around instead of crossing a page boundary
     */
    public static short MODE_INDIRECT() {
        //address
        int address = getNextWord();
        int operandB = (address);
        /**
         * note:this is the stupid 6502 page boundary bug instead of fetching
         * 0x00FF and 0x0100, it fetches 0x00FF and 0x0000
         */
        int operandAWithPageBoundaryFix = ((address & 0xFF) == 0xFF ? address ^ 0xFF : address + 1);
        byte a = (byte) getROMByte((short) operandAWithPageBoundaryFix);//little big endian
        byte b = (byte) getROMByte((short) operandB);

        address = ((a & 0xFF) << 8) + (b & 0xFF);

        return (short) address;
    }

    /**
     * @return the BYTE at the indexed indirect X address(next byte plus X
     * register results in a location in the zero page, which together with the
     * next location serve as the indirect x address)
     */
    public static short MODE_INDIRECT_X() {
        return getROMByte(ADDRESS_INDIRECT_X());
    }

    /**
     * @return the BYTE at the indirect indexed Y address(next byte as a
     * location in the zero page, which together with the next location plus the
     * Y register serve as the indirect y address)
     */
    public static short MODE_INDIRECT_Y() {
        int pc = Registers.getUnsignerPC();
        short ROMByte = getROMByte(ADDRESS_INDIRECT_Y());
        isPageCrossed(pc);
        return ROMByte;
    }

    /**@return the next <b>SIGNED</b> byte*/
    public static byte MODE_RELATIVE() {
        //relative mode is used by branches, and they need a signed byte
        int pc = Registers.getUnsignerPC();
        byte nextByte = getNextByte();
        isPageCrossed(pc);
        return nextByte;
    }

    /**@return the Accumulator register*/
    public static byte MODE_ACCUMULATOR() {
        return Registers.A;
    }

    /**@return the unsigned X register*/
    private static short getX() {
        return (short) (Registers.X & 0xFF);
    }

    /**@return the unsigned Y register*/
    private static short getY() {
        return (short) (Registers.Y & 0xFF);
    }

    private static short getROMByte(short address) {
        return readByte(address);
    }
    private static boolean pageBoundaryCrossed = false;

    /**
     * @return 0 if page boundary not crossed or the input cycles if crossed
     */
    public static int getPageCrossCycles(int cycles) {
        if (pageBoundaryCrossed) {
            pageBoundaryCrossed = false;
            return cycles;
        }
        return 0;
    }

    private static void isPageCrossed(int pcBeforeIncrement) {
        if ((pcBeforeIncrement & 0xFF) > (Registers.getUnsignerPC() & 0xFF)) {//page crossed
            pageBoundaryCrossed = true;
        } else {
            pageBoundaryCrossed = false;
        }
    }

    /**
     * A D D R E S S I N G
     * @return address
     */
    /**
     * @return the address from the next 2 bytes
     */
    public static short ADDRESS_ABSOLUTE() {
        return getNextWord();
    }

    /**
     * @return the address from the next 2 bytes plus the X register
     */
    public static short ADDRESS_ABSOLUTE_X() {
        return (short) ((getNextWord() + getX()) & 0xFFFF);
    }

    public static short ADDRESS_ABSOLUTE_X_DUMMY_READ() {
        short address = getNextWord();

        int absoluteAddress = ((address + getX()) & 0xFFFF);
        int dummyAbsoluteAddress = ((address & 0xFF00) | (absoluteAddress & 0xFF));

        if (dummyAbsoluteAddress != absoluteAddress) {
            CPU.skipInstruction = true;//skip the instruction
            Registers.PC++;//move to next position
        }
        return (short) dummyAbsoluteAddress;
    }

    /** @return the address from the next 2 bytes plus the Y register*/
    public static short ADDRESS_ABSOLUTE_Y() {
        return (short) ((getNextWord() + getY()) & 0xFFFF);
    }

    /**@return unsigned next byte*/
    public static short ADDRESS_ZERO_PAGE() {
        return (short) (getNextByte() & 0xFF);
    }

    /**@return unsigned next byte plus X register*/
    public static short ADDRESS_ZERO_PAGE_X() {
        return (short) ((getNextByte() + getX()) & 0xFF);
    }

    /**@return unsigned next byte plus Y register*/
    public static short ADDRESS_ZERO_PAGE_Y() {
        return (short) ((getNextByte() + getY()) & 0xFF);
    }
    private static final int ZERO_PAGE_WRAP_AROUND = 0x100;//TODO:remove from indirect?

    /**
     * @return the indirect x address, which is made of of a location in the
     * zero page which is addressed by the next byte plus X register, together
     * with the next zero page location
     */
    public static short ADDRESS_INDIRECT_X() {
        int nextBytePlusX = (getNextByte() & 0xFF) + getX();
        int operandA = (nextBytePlusX + 1) % ZERO_PAGE_WRAP_AROUND;//little big endian
        int operandB = (nextBytePlusX) % ZERO_PAGE_WRAP_AROUND;
        byte a = (byte) getROMByte((short) operandA);//little big endian
        byte b = (byte) getROMByte((short) operandB);

        //address
        int address = ((a & 0xFF) << 8) + (b & 0xFF);
        return (short) address;
    }

    /**
     * @return the indirect y address, which consists of the next byte as a
     * location in the zero page, together with the next location(in the zero
     * page) plus the Y register
     */
    public static short ADDRESS_INDIRECT_Y() {
        int nextByte = (getNextByte() & 0xFF);
        int operandA = (nextByte + 1) % ZERO_PAGE_WRAP_AROUND;//little big endian
        int operandB = (nextByte) % ZERO_PAGE_WRAP_AROUND;
        byte a = (byte) getROMByte((short) operandA);//little big endian
        byte b = (byte) getROMByte((short) operandB);

        //address
        int address = ((a & 0xFF) << 8) + (b & 0xFF);
        address += getY();
        return (short) address;
    }
}
